/**
 * Copyright 2010-present Facebook.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook;

import android.Manifest;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.text.TextUtils;
import android.webkit.CookieSyncManager;
import com.facebook.android.R;
import com.facebook.internal.AnalyticsEvents;
import com.facebook.internal.NativeProtocol;
import com.facebook.internal.ServerProtocol;
import com.facebook.internal.Utility;
import com.facebook.model.GraphUser;
import com.facebook.widget.WebDialog;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

class AuthorizationClient implements Serializable
{
  private static final long serialVersionUID = 1L;
  private static final String TAG = "Facebook-AuthorizationClient";
  private static final String WEB_VIEW_AUTH_HANDLER_STORE =
      "com.facebook.AuthorizationClient.WebViewAuthHandler.TOKEN_STORE_KEY";
  private static final String WEB_VIEW_AUTH_HANDLER_TOKEN_KEY = "TOKEN";

  // Constants for logging login-related data. Some of these are only used by Session, but grouped here for
  // maintainability.
  private static final String EVENT_NAME_LOGIN_METHOD_START = "fb_mobile_login_method_start";
  private static final String EVENT_NAME_LOGIN_METHOD_COMPLETE = "fb_mobile_login_method_complete";
  private static final String EVENT_PARAM_METHOD_RESULT_SKIPPED = "skipped";
  static final String EVENT_NAME_LOGIN_START = "fb_mobile_login_start";
  static final String EVENT_NAME_LOGIN_COMPLETE = "fb_mobile_login_complete";
  // Note: to ensure stability of column mappings across the four different event types, we prepend a column
  // index to each name, and we log all columns with all events, even if they are empty.
  static final String EVENT_PARAM_AUTH_LOGGER_ID = "0_auth_logger_id";
  static final String EVENT_PARAM_TIMESTAMP = "1_timestamp_ms";
  static final String EVENT_PARAM_LOGIN_RESULT = "2_result";
  static final String EVENT_PARAM_METHOD = "3_method";
  static final String EVENT_PARAM_ERROR_CODE = "4_error_code";
  static final String EVENT_PARAM_ERROR_MESSAGE = "5_error_message";
  static final String EVENT_PARAM_EXTRAS = "6_extras";
  static final String EVENT_EXTRAS_TRY_LOGIN_ACTIVITY = "try_login_activity";
  static final String EVENT_EXTRAS_TRY_LEGACY = "try_legacy";
  static final String EVENT_EXTRAS_LOGIN_BEHAVIOR = "login_behavior";
  static final String EVENT_EXTRAS_REQUEST_CODE = "request_code";
  static final String EVENT_EXTRAS_IS_LEGACY = "is_legacy";
  static final String EVENT_EXTRAS_PERMISSIONS = "permissions";
  static final String EVENT_EXTRAS_DEFAULT_AUDIENCE = "default_audience";
  static final String EVENT_EXTRAS_MISSING_INTERNET_PERMISSION = "no_internet_permission";
  static final String EVENT_EXTRAS_NOT_TRIED = "not_tried";
  static final String EVENT_EXTRAS_NEW_PERMISSIONS = "new_permissions";
  static final String EVENT_EXTRAS_SERVICE_DISABLED = "service_disabled";
  static final String EVENT_EXTRAS_APP_CALL_ID = "call_id";
  static final String EVENT_EXTRAS_PROTOCOL_VERSION = "protocol_version";
  static final String EVENT_EXTRAS_WRITE_PRIVACY = "write_privacy";

  List<AuthHandler> handlersToTry;
  AuthHandler currentHandler;
  transient Context context;
  transient StartActivityDelegate startActivityDelegate;
  transient OnCompletedListener onCompletedListener;
  transient BackgroundProcessingListener backgroundProcessingListener;
  transient boolean checkedInternetPermission;
  AuthorizationRequest pendingRequest;
  Map<String, String> loggingExtras;
  private transient AppEventsLogger appEventsLogger;

  interface OnCompletedListener
  {
    void onCompleted(Result result);
  }

  interface BackgroundProcessingListener
  {
    void onBackgroundProcessingStarted();

    void onBackgroundProcessingStopped();
  }

  interface StartActivityDelegate
  {
    public void startActivityForResult(Intent intent, int requestCode);

    public Activity getActivityContext();
  }

  void setContext(final Context context)
  {
    this.context = context;
    // We rely on individual requests to tell us how to start an activity.
    startActivityDelegate = null;
  }

  void setContext(final Activity activity)
  {
    this.context = activity;

    // If we are used in the context of an activity, we will always use that activity to
    // call startActivityForResult.
    startActivityDelegate = new StartActivityDelegate()
    {
      @Override
      public void startActivityForResult(Intent intent, int requestCode)
      {
        activity.startActivityForResult(intent, requestCode);
      }

      @Override
      public Activity getActivityContext()
      {
        return activity;
      }
    };
  }

  void startOrContinueAuth(AuthorizationRequest request)
  {
    if (getInProgress())
    {
      continueAuth();
    }
    else
    {
      authorize(request);
    }
  }

  void authorize(AuthorizationRequest request)
  {
    if (request == null)
    {
      return;
    }

    if (pendingRequest != null)
    {
      throw new FacebookException("Attempted to authorize while a request is pending.");
    }

    if (request.needsNewTokenValidation() && !checkInternetPermission())
    {
      // We're going to need INTERNET permission later and don't have it, so fail early.
      return;
    }
    pendingRequest = request;
    handlersToTry = getHandlerTypes(request);
    tryNextHandler();
  }

  void continueAuth()
  {
    if (pendingRequest == null || currentHandler == null)
    {
      throw new FacebookException("Attempted to continue authorization without a pending request.");
    }

    if (currentHandler.needsRestart())
    {
      currentHandler.cancel();
      tryCurrentHandler();
    }
  }

  boolean getInProgress()
  {
    return pendingRequest != null && currentHandler != null;
  }

  void cancelCurrentHandler()
  {
    if (currentHandler != null)
    {
      currentHandler.cancel();
    }
  }

  boolean onActivityResult(int requestCode, int resultCode, Intent data)
  {
    if (requestCode == pendingRequest.getRequestCode())
    {
      return currentHandler.onActivityResult(requestCode, resultCode, data);
    }
    return false;
  }

  private List<AuthHandler> getHandlerTypes(AuthorizationRequest request)
  {
    ArrayList<AuthHandler> handlers = new ArrayList<AuthHandler>();

    final SessionLoginBehavior behavior = request.getLoginBehavior();
    if (behavior.allowsKatanaAuth())
    {
      if (!request.isLegacy())
      {
        handlers.add(new GetTokenAuthHandler());
      }
      handlers.add(new KatanaProxyAuthHandler());
    }

    if (behavior.allowsWebViewAuth())
    {
      handlers.add(new WebViewAuthHandler());
    }

    return handlers;
  }

  boolean checkInternetPermission()
  {
    if (checkedInternetPermission)
    {
      return true;
    }

    int permissionCheck = checkPermission(Manifest.permission.INTERNET);
    if (permissionCheck != PackageManager.PERMISSION_GRANTED)
    {
      String errorType = context.getString(R.string.com_facebook_internet_permission_error_title);
      String errorDescription = context.getString(R.string.com_facebook_internet_permission_error_message);
      complete(Result.createErrorResult(pendingRequest, errorType, errorDescription));

      return false;
    }

    checkedInternetPermission = true;
    return true;
  }

  void tryNextHandler()
  {
    if (currentHandler != null)
    {
      logAuthorizationMethodComplete(currentHandler.getNameForLogging(), EVENT_PARAM_METHOD_RESULT_SKIPPED,
          null, null, currentHandler.methodLoggingExtras);
    }

    while (handlersToTry != null && !handlersToTry.isEmpty())
    {
      currentHandler = handlersToTry.remove(0);

      boolean started = tryCurrentHandler();

      if (started)
      {
        return;
      }
    }

    if (pendingRequest != null)
    {
      // We went through all handlers without successfully attempting an auth.
      completeWithFailure();
    }
  }

  private void completeWithFailure()
  {
    complete(Result.createErrorResult(pendingRequest, "Login attempt failed.", null));
  }

  private void addLoggingExtra(String key, String value, boolean accumulate)
  {
    if (loggingExtras == null)
    {
      loggingExtras = new HashMap<String, String>();
    }
    if (loggingExtras.containsKey(key) && accumulate)
    {
      value = loggingExtras.get(key) + "," + value;
    }
    loggingExtras.put(key, value);
  }

  boolean tryCurrentHandler()
  {
    if (currentHandler.needsInternetPermission() && !checkInternetPermission())
    {
      addLoggingExtra(EVENT_EXTRAS_MISSING_INTERNET_PERMISSION, AppEventsConstants.EVENT_PARAM_VALUE_YES,
          false);
      return false;
    }

    boolean tried = currentHandler.tryAuthorize(pendingRequest);
    if (tried)
    {
      logAuthorizationMethodStart(currentHandler.getNameForLogging());
    }
    else
    {
      // We didn't try it, so we don't get any other completion notification -- log that we skipped it.
      addLoggingExtra(EVENT_EXTRAS_NOT_TRIED, currentHandler.getNameForLogging(), true);
    }

    return tried;
  }

  void completeAndValidate(Result outcome)
  {
    // Do we need to validate a successful result (as in the case of a reauth)?
    if (outcome.token != null && pendingRequest.needsNewTokenValidation())
    {
      validateSameFbidAndFinish(outcome);
    }
    else
    {
      // We're done, just notify the listener.
      complete(outcome);
    }
  }

  void complete(Result outcome)
  {
    // This might be null if, for some reason, none of the handlers were successfully tried (in which case
    // we already logged that).
    if (currentHandler != null)
    {
      logAuthorizationMethodComplete(currentHandler.getNameForLogging(), outcome,
          currentHandler.methodLoggingExtras);
    }

    if (loggingExtras != null)
    {
      // Pass this back to the caller for logging at the aggregate level.
      outcome.loggingExtras = loggingExtras;
    }

    handlersToTry = null;
    currentHandler = null;
    pendingRequest = null;
    loggingExtras = null;

    notifyOnCompleteListener(outcome);
  }

  OnCompletedListener getOnCompletedListener()
  {
    return onCompletedListener;
  }

  void setOnCompletedListener(OnCompletedListener onCompletedListener)
  {
    this.onCompletedListener = onCompletedListener;
  }

  BackgroundProcessingListener getBackgroundProcessingListener()
  {
    return backgroundProcessingListener;
  }

  void setBackgroundProcessingListener(BackgroundProcessingListener backgroundProcessingListener)
  {
    this.backgroundProcessingListener = backgroundProcessingListener;
  }

  StartActivityDelegate getStartActivityDelegate()
  {
    if (startActivityDelegate != null)
    {
      return startActivityDelegate;
    }
    else
      if (pendingRequest != null)
      {
        // Wrap the request's delegate in our own.
        return new StartActivityDelegate()
        {
          @Override
          public void startActivityForResult(Intent intent, int requestCode)
          {
            pendingRequest.getStartActivityDelegate().startActivityForResult(intent, requestCode);
          }

          @Override
          public Activity getActivityContext()
          {
            return pendingRequest.getStartActivityDelegate().getActivityContext();
          }
        };
      }
    return null;
  }

  int checkPermission(String permission)
  {
    return context.checkCallingOrSelfPermission(permission);
  }

  void validateSameFbidAndFinish(Result pendingResult)
  {
    if (pendingResult.token == null)
    {
      throw new FacebookException("Can't validate without a token");
    }

    RequestBatch batch = createReauthValidationBatch(pendingResult);

    notifyBackgroundProcessingStart();

    batch.executeAsync();
  }

  RequestBatch createReauthValidationBatch(final Result pendingResult)
  {
    // We need to ensure that the token we got represents the same fbid as the old one. We issue
    // a "me" request using the current token, a "me" request using the new token, and a "me/permissions"
    // request using the current token to get the permissions of the user.

    final ArrayList<String> fbids = new ArrayList<String>();
    final ArrayList<String> tokenPermissions = new ArrayList<String>();
    final String newToken = pendingResult.token.getToken();

    Request.Callback meCallback = new Request.Callback()
    {
      @Override
      public void onCompleted(Response response)
      {
        try
        {
          GraphUser user = response.getGraphObjectAs(GraphUser.class);
          if (user != null)
          {
            fbids.add(user.getId());
          }
        }
        catch (Exception ex)
        {
        }
      }
    };

    String validateSameFbidAsToken = pendingRequest.getPreviousAccessToken();
    Request requestCurrentTokenMe = createGetProfileIdRequest(validateSameFbidAsToken);
    requestCurrentTokenMe.setCallback(meCallback);

    Request requestNewTokenMe = createGetProfileIdRequest(newToken);
    requestNewTokenMe.setCallback(meCallback);

    Request requestCurrentTokenPermissions = createGetPermissionsRequest(validateSameFbidAsToken);
    requestCurrentTokenPermissions.setCallback(new Request.Callback()
    {
      @Override
      public void onCompleted(Response response)
      {
        try
        {
          List<String> permissions = Session.handlePermissionResponse(null, response);
          if (permissions != null)
          {
            tokenPermissions.addAll(permissions);
          }
        }
        catch (Exception ex)
        {
        }
      }
    });

    RequestBatch batch = new RequestBatch(requestCurrentTokenMe, requestNewTokenMe,
        requestCurrentTokenPermissions);
    batch.setBatchApplicationId(pendingRequest.getApplicationId());
    batch.addCallback(new RequestBatch.Callback()
    {
      @Override
      public void onBatchCompleted(RequestBatch batch)
      {
        try
        {
          Result result = null;
          if (fbids.size() == 2 && fbids.get(0) != null && fbids.get(1) != null &&
              fbids.get(0).equals(fbids.get(1)))
          {
            // Modify the token to have the right permission set.
            AccessToken tokenWithPermissions = AccessToken
                .createFromTokenWithRefreshedPermissions(pendingResult.token,
                    tokenPermissions);
            result = Result.createTokenResult(pendingRequest, tokenWithPermissions);
          }
          else
          {
            result = Result
                .createErrorResult(pendingRequest, "User logged in as different Facebook user.", null);
          }
          complete(result);
        }
        catch (Exception ex)
        {
          complete(Result.createErrorResult(pendingRequest, "Caught exception", ex.getMessage()));
        }
        finally
        {
          notifyBackgroundProcessingStop();
        }
      }
    });

    return batch;
  }

  Request createGetPermissionsRequest(String accessToken)
  {
    Bundle parameters = new Bundle();
    parameters.putString("access_token", accessToken);
    return new Request(null, "me/permissions", parameters, HttpMethod.GET, null);
  }

  Request createGetProfileIdRequest(String accessToken)
  {
    Bundle parameters = new Bundle();
    parameters.putString("fields", "id");
    parameters.putString("access_token", accessToken);
    return new Request(null, "me", parameters, HttpMethod.GET, null);
  }

  private AppEventsLogger getAppEventsLogger()
  {
    if (appEventsLogger == null || appEventsLogger.getApplicationId() != pendingRequest.getApplicationId())
    {
      appEventsLogger = AppEventsLogger.newLogger(context, pendingRequest.getApplicationId());
    }
    return appEventsLogger;
  }

  private void notifyOnCompleteListener(Result outcome)
  {
    if (onCompletedListener != null)
    {
      onCompletedListener.onCompleted(outcome);
    }
  }

  private void notifyBackgroundProcessingStart()
  {
    if (backgroundProcessingListener != null)
    {
      backgroundProcessingListener.onBackgroundProcessingStarted();
    }
  }

  private void notifyBackgroundProcessingStop()
  {
    if (backgroundProcessingListener != null)
    {
      backgroundProcessingListener.onBackgroundProcessingStopped();
    }
  }

  private void logAuthorizationMethodStart(String method)
  {
    Bundle bundle = newAuthorizationLoggingBundle(pendingRequest.getAuthId());
    bundle.putLong(EVENT_PARAM_TIMESTAMP, System.currentTimeMillis());
    bundle.putString(EVENT_PARAM_METHOD, method);

    getAppEventsLogger().logSdkEvent(EVENT_NAME_LOGIN_METHOD_START, null, bundle);
  }

  private void logAuthorizationMethodComplete(String method, Result result, Map<String, String> loggingExtras)
  {
    logAuthorizationMethodComplete(method, result.code.getLoggingValue(), result.errorMessage, result.errorCode,
        loggingExtras);
  }

  private void logAuthorizationMethodComplete(String method, String result, String errorMessage, String errorCode,
                                              Map<String, String> loggingExtras)
  {
    Bundle bundle = null;
    if (pendingRequest == null)
    {
      // We don't expect this to happen, but if it does, log an event for diagnostic purposes.
      bundle = newAuthorizationLoggingBundle("");
      bundle.putString(EVENT_PARAM_LOGIN_RESULT, Result.Code.ERROR.getLoggingValue());
      bundle.putString(EVENT_PARAM_ERROR_MESSAGE,
          "Unexpected call to logAuthorizationMethodComplete with null pendingRequest.");
    }
    else
    {
      bundle = newAuthorizationLoggingBundle(pendingRequest.getAuthId());
      if (result != null)
      {
        bundle.putString(EVENT_PARAM_LOGIN_RESULT, result);
      }
      if (errorMessage != null)
      {
        bundle.putString(EVENT_PARAM_ERROR_MESSAGE, errorMessage);
      }
      if (errorCode != null)
      {
        bundle.putString(EVENT_PARAM_ERROR_CODE, errorCode);
      }
      if (loggingExtras != null && !loggingExtras.isEmpty())
      {
        JSONObject jsonObject = new JSONObject(loggingExtras);
        bundle.putString(EVENT_PARAM_EXTRAS, jsonObject.toString());
      }
    }
    bundle.putString(EVENT_PARAM_METHOD, method);
    bundle.putLong(EVENT_PARAM_TIMESTAMP, System.currentTimeMillis());

    getAppEventsLogger().logSdkEvent(EVENT_NAME_LOGIN_METHOD_COMPLETE, null, bundle);
  }

  static Bundle newAuthorizationLoggingBundle(String authLoggerId)
  {
    // We want to log all parameters for all events, to ensure stability of columns across different event types.
    Bundle bundle = new Bundle();
    bundle.putLong(EVENT_PARAM_TIMESTAMP, System.currentTimeMillis());
    bundle.putString(EVENT_PARAM_AUTH_LOGGER_ID, authLoggerId);
    bundle.putString(EVENT_PARAM_METHOD, "");
    bundle.putString(EVENT_PARAM_LOGIN_RESULT, "");
    bundle.putString(EVENT_PARAM_ERROR_MESSAGE, "");
    bundle.putString(EVENT_PARAM_ERROR_CODE, "");
    bundle.putString(EVENT_PARAM_EXTRAS, "");
    return bundle;
  }

  abstract class AuthHandler implements Serializable
  {
    private static final long serialVersionUID = 1L;

    Map<String, String> methodLoggingExtras;

    abstract boolean tryAuthorize(AuthorizationRequest request);

    abstract String getNameForLogging();

    boolean onActivityResult(int requestCode, int resultCode, Intent data)
    {
      return false;
    }

    boolean needsRestart()
    {
      return false;
    }

    boolean needsInternetPermission()
    {
      return false;
    }

    void cancel()
    {
    }

    protected void addLoggingExtra(String key, Object value)
    {
      if (methodLoggingExtras == null)
      {
        methodLoggingExtras = new HashMap<String, String>();
      }
      methodLoggingExtras.put(key, value == null ? null : value.toString());
    }
  }

  class WebViewAuthHandler extends AuthHandler
  {
    private static final long serialVersionUID = 1L;
    private transient WebDialog loginDialog;
    private String applicationId;
    private String e2e;

    @Override
    String getNameForLogging()
    {
      return "web_view";
    }

    @Override
    boolean needsRestart()
    {
      // Because we are presenting WebView UI within the current context, we need to explicitly
      // restart the process if the context goes away and is recreated.
      return true;
    }

    @Override
    boolean needsInternetPermission()
    {
      return true;
    }

    @Override
    void cancel()
    {
      if (loginDialog != null)
      {
        loginDialog.dismiss();
        loginDialog = null;
      }
    }

    @Override
    boolean tryAuthorize(final AuthorizationRequest request)
    {
      applicationId = request.getApplicationId();
      Bundle parameters = new Bundle();
      if (!Utility.isNullOrEmpty(request.getPermissions()))
      {
        String scope = TextUtils.join(",", request.getPermissions());
        parameters.putString(ServerProtocol.DIALOG_PARAM_SCOPE, scope);
        addLoggingExtra(ServerProtocol.DIALOG_PARAM_SCOPE, scope);
      }

      String previousToken = request.getPreviousAccessToken();
      if (!Utility.isNullOrEmpty(previousToken) && (previousToken.equals(loadCookieToken())))
      {
        parameters.putString(ServerProtocol.DIALOG_PARAM_ACCESS_TOKEN, previousToken);
        // Don't log the actual access token, just its presence or absence.
        addLoggingExtra(ServerProtocol.DIALOG_PARAM_ACCESS_TOKEN, AppEventsConstants.EVENT_PARAM_VALUE_YES);
      }
      else
      {
        // The call to clear cookies will create the first instance of CookieSyncManager if necessary
        Utility.clearFacebookCookies(context);
        addLoggingExtra(ServerProtocol.DIALOG_PARAM_ACCESS_TOKEN, AppEventsConstants.EVENT_PARAM_VALUE_NO);
      }

      WebDialog.OnCompleteListener listener = new WebDialog.OnCompleteListener()
      {
        @Override
        public void onComplete(Bundle values, FacebookException error)
        {
          onWebDialogComplete(request, values, error);
        }
      };

      e2e = getE2E();
      addLoggingExtra(ServerProtocol.DIALOG_PARAM_E2E, e2e);

      WebDialog.Builder builder =
          new AuthDialogBuilder(getStartActivityDelegate().getActivityContext(), applicationId, parameters)
              .setE2E(e2e)
              .setIsRerequest(request.isRerequest())
              .setOnCompleteListener(listener);
      loginDialog = builder.build();
      loginDialog.show();

      return true;
    }

    void onWebDialogComplete(AuthorizationRequest request, Bundle values,
                             FacebookException error)
    {
      Result outcome;
      if (values != null)
      {
        // Actual e2e we got from the dialog should be used for logging.
        if (values.containsKey(ServerProtocol.DIALOG_PARAM_E2E))
        {
          e2e = values.getString(ServerProtocol.DIALOG_PARAM_E2E);
        }

        AccessToken token = AccessToken
            .createFromWebBundle(request.getPermissions(), values, AccessTokenSource.WEB_VIEW);
        outcome = Result.createTokenResult(pendingRequest, token);

        // Ensure any cookies set by the dialog are saved
        // This is to work around a bug where CookieManager may fail to instantiate if CookieSyncManager
        // has never been created.
        CookieSyncManager syncManager = CookieSyncManager.createInstance(context);
        syncManager.sync();
        saveCookieToken(token.getToken());
      }
      else
      {
        if (error instanceof FacebookOperationCanceledException)
        {
          outcome = Result.createCancelResult(pendingRequest, "User canceled log in.");
        }
        else
        {
          // Something went wrong, don't log a completion event since it will skew timing results.
          e2e = null;

          String errorCode = null;
          String errorMessage = error.getMessage();
          if (error instanceof FacebookServiceException)
          {
            FacebookRequestError requestError = ((FacebookServiceException) error).getRequestError();
            errorCode = String.format("%d", requestError.getErrorCode());
            errorMessage = requestError.toString();
          }
          outcome = Result.createErrorResult(pendingRequest, null, errorMessage, errorCode);
        }
      }

      if (!Utility.isNullOrEmpty(e2e))
      {
        logWebLoginCompleted(applicationId, e2e);
      }

      completeAndValidate(outcome);
    }

    private void saveCookieToken(String token)
    {
      Context context = getStartActivityDelegate().getActivityContext();
      SharedPreferences sharedPreferences = context.getSharedPreferences(
          WEB_VIEW_AUTH_HANDLER_STORE,
          Context.MODE_PRIVATE);
      SharedPreferences.Editor editor = sharedPreferences.edit();
      editor.putString(WEB_VIEW_AUTH_HANDLER_TOKEN_KEY, token);
      if (!editor.commit())
      {
        Utility.logd(TAG, "Could not update saved web view auth handler token.");
      }
    }

    private String loadCookieToken()
    {
      Context context = getStartActivityDelegate().getActivityContext();
      SharedPreferences sharedPreferences = context.getSharedPreferences(
          WEB_VIEW_AUTH_HANDLER_STORE,
          Context.MODE_PRIVATE);
      return sharedPreferences.getString(WEB_VIEW_AUTH_HANDLER_TOKEN_KEY, "");
    }
  }

  class GetTokenAuthHandler extends AuthHandler
  {
    private static final long serialVersionUID = 1L;
    private transient GetTokenClient getTokenClient;

    @Override
    String getNameForLogging()
    {
      return "get_token";
    }

    @Override
    void cancel()
    {
      if (getTokenClient != null)
      {
        getTokenClient.cancel();
        getTokenClient = null;
      }
    }

    @Override
    boolean needsRestart()
    {
      // if the getTokenClient is null, that means an orientation change has occurred, and we need
      // to recreate the GetTokenClient, so return true to indicate we need a restart
      return getTokenClient == null;
    }

    boolean tryAuthorize(final AuthorizationRequest request)
    {
      getTokenClient = new GetTokenClient(context, request.getApplicationId());
      if (!getTokenClient.start())
      {
        return false;
      }

      notifyBackgroundProcessingStart();

      GetTokenClient.CompletedListener callback = new GetTokenClient.CompletedListener()
      {
        @Override
        public void completed(Bundle result)
        {
          getTokenCompleted(request, result);
        }
      };

      getTokenClient.setCompletedListener(callback);
      return true;
    }

    void getTokenCompleted(AuthorizationRequest request, Bundle result)
    {
      getTokenClient = null;

      notifyBackgroundProcessingStop();

      if (result != null)
      {
        ArrayList<String> currentPermissions = result.getStringArrayList(NativeProtocol.EXTRA_PERMISSIONS);
        List<String> permissions = request.getPermissions();
        if ((currentPermissions != null) &&
            ((permissions == null) || currentPermissions.containsAll(permissions)))
        {
          // We got all the permissions we needed, so we can complete the auth now.
          AccessToken token = AccessToken
              .createFromNativeLogin(result, AccessTokenSource.FACEBOOK_APPLICATION_SERVICE);
          Result outcome = Result.createTokenResult(pendingRequest, token);
          completeAndValidate(outcome);
          return;
        }

        // We didn't get all the permissions we wanted, so update the request with just the permissions
        // we still need.
        List<String> newPermissions = new ArrayList<String>();
        for (String permission : permissions)
        {
          if (!currentPermissions.contains(permission))
          {
            newPermissions.add(permission);
          }
        }
        if (!newPermissions.isEmpty())
        {
          addLoggingExtra(EVENT_EXTRAS_NEW_PERMISSIONS, TextUtils.join(",", newPermissions));
        }

        request.setPermissions(newPermissions);
      }

      tryNextHandler();
    }
  }

  abstract class KatanaAuthHandler extends AuthHandler
  {
    private static final long serialVersionUID = 1L;

    protected boolean tryIntent(Intent intent, int requestCode)
    {
      if (intent == null)
      {
        return false;
      }

      try
      {
        getStartActivityDelegate().startActivityForResult(intent, requestCode);
      }
      catch (ActivityNotFoundException e)
      {
        // We don't expect this to happen, since we've already validated the intent and bailed out before
        // now if it couldn't be resolved.
        return false;
      }

      return true;
    }
  }

  class KatanaProxyAuthHandler extends KatanaAuthHandler
  {
    private static final long serialVersionUID = 1L;
    private String applicationId;

    @Override
    String getNameForLogging()
    {
      return "katana_proxy_auth";
    }

    @Override
    boolean tryAuthorize(AuthorizationRequest request)
    {
      applicationId = request.getApplicationId();

      String e2e = getE2E();
      Intent intent = NativeProtocol.createProxyAuthIntent(context, request.getApplicationId(),
          request.getPermissions(), e2e, request.isRerequest());

      addLoggingExtra(ServerProtocol.DIALOG_PARAM_E2E, e2e);

      return tryIntent(intent, request.getRequestCode());
    }

    @Override
    boolean onActivityResult(int requestCode, int resultCode, Intent data)
    {
      // Handle stuff
      Result outcome;

      if (data == null)
      {
        // This happens if the user presses 'Back'.
        outcome = Result.createCancelResult(pendingRequest, "Operation canceled");
      }
      else
        if (resultCode == Activity.RESULT_CANCELED)
        {
          outcome = Result.createCancelResult(pendingRequest, data.getStringExtra("error"));
        }
        else
          if (resultCode != Activity.RESULT_OK)
          {
            outcome = Result.createErrorResult(pendingRequest, "Unexpected resultCode from authorization.", null);
          }
          else
          {
            outcome = handleResultOk(data);
          }

      if (outcome != null)
      {
        completeAndValidate(outcome);
      }
      else
      {
        tryNextHandler();
      }
      return true;
    }

    private Result handleResultOk(Intent data)
    {
      Bundle extras = data.getExtras();
      String error = extras.getString("error");
      if (error == null)
      {
        error = extras.getString("error_type");
      }
      String errorCode = extras.getString("error_code");
      String errorMessage = extras.getString("error_message");
      if (errorMessage == null)
      {
        errorMessage = extras.getString("error_description");
      }

      String e2e = extras.getString(NativeProtocol.FACEBOOK_PROXY_AUTH_E2E_KEY);
      if (!Utility.isNullOrEmpty(e2e))
      {
        logWebLoginCompleted(applicationId, e2e);
      }

      if (error == null && errorCode == null && errorMessage == null)
      {
        AccessToken token = AccessToken.createFromWebBundle(pendingRequest.getPermissions(), extras,
            AccessTokenSource.FACEBOOK_APPLICATION_WEB);
        return Result.createTokenResult(pendingRequest, token);
      }
      else
        if (ServerProtocol.errorsProxyAuthDisabled.contains(error))
        {
          return null;
        }
        else
          if (ServerProtocol.errorsUserCanceled.contains(error))
          {
            return Result.createCancelResult(pendingRequest, null);
          }
          else
          {
            return Result.createErrorResult(pendingRequest, error, errorMessage, errorCode);
          }
    }
  }

  private static String getE2E()
  {
    JSONObject e2e = new JSONObject();
    try
    {
      e2e.put("init", System.currentTimeMillis());
    }
    catch (JSONException e)
    {
    }
    return e2e.toString();
  }

  private void logWebLoginCompleted(String applicationId, String e2e)
  {
    AppEventsLogger appEventsLogger = AppEventsLogger.newLogger(context, applicationId);

    Bundle parameters = new Bundle();
    parameters.putString(AnalyticsEvents.PARAMETER_WEB_LOGIN_E2E, e2e);
    parameters.putLong(AnalyticsEvents.PARAMETER_WEB_LOGIN_SWITCHBACK_TIME, System.currentTimeMillis());
    parameters.putString(AnalyticsEvents.PARAMETER_APP_ID, applicationId);

    appEventsLogger.logSdkEvent(AnalyticsEvents.EVENT_WEB_LOGIN_COMPLETE, null, parameters);
  }

  static class AuthDialogBuilder extends WebDialog.Builder
  {
    private static final String OAUTH_DIALOG = "oauth";
    static final String REDIRECT_URI = "fbconnect://success";
    private String e2e;
    private boolean isRerequest;

    public AuthDialogBuilder(Context context, String applicationId, Bundle parameters)
    {
      super(context, applicationId, OAUTH_DIALOG, parameters);
    }

    public AuthDialogBuilder setE2E(String e2e)
    {
      this.e2e = e2e;
      return this;
    }

    public AuthDialogBuilder setIsRerequest(boolean isRerequest)
    {
      this.isRerequest = isRerequest;
      return this;
    }

    @Override
    public WebDialog build()
    {
      Bundle parameters = getParameters();
      parameters.putString(ServerProtocol.DIALOG_PARAM_REDIRECT_URI, REDIRECT_URI);
      parameters.putString(ServerProtocol.DIALOG_PARAM_CLIENT_ID, getApplicationId());
      parameters.putString(ServerProtocol.DIALOG_PARAM_E2E, e2e);
      parameters.putString(ServerProtocol.DIALOG_PARAM_RESPONSE_TYPE, ServerProtocol.DIALOG_RESPONSE_TYPE_TOKEN);
      parameters.putString(ServerProtocol.DIALOG_PARAM_RETURN_SCOPES, ServerProtocol.DIALOG_RETURN_SCOPES_TRUE);

      // Only set the rerequest auth type for non legacy requests
      if (isRerequest && !Settings.getPlatformCompatibilityEnabled())
      {
        parameters.putString(ServerProtocol.DIALOG_PARAM_AUTH_TYPE, ServerProtocol.DIALOG_REREQUEST_AUTH_TYPE);
      }

      return new WebDialog(getContext(), OAUTH_DIALOG, parameters, getTheme(), getListener());
    }
  }

  static class AuthorizationRequest implements Serializable
  {
    private static final long serialVersionUID = 1L;

    private transient final StartActivityDelegate startActivityDelegate;
    private final SessionLoginBehavior loginBehavior;
    private final int requestCode;
    private boolean isLegacy = false;
    private List<String> permissions;
    private final SessionDefaultAudience defaultAudience;
    private final String applicationId;
    private final String previousAccessToken;
    private final String authId;
    private boolean isRerequest = false;

    AuthorizationRequest(SessionLoginBehavior loginBehavior, int requestCode, boolean isLegacy,
                         List<String> permissions, SessionDefaultAudience defaultAudience, String applicationId,
                         String validateSameFbidAsToken, StartActivityDelegate startActivityDelegate, String authId)
    {
      this.loginBehavior = loginBehavior;
      this.requestCode = requestCode;
      this.isLegacy = isLegacy;
      this.permissions = permissions;
      this.defaultAudience = defaultAudience;
      this.applicationId = applicationId;
      this.previousAccessToken = validateSameFbidAsToken;
      this.startActivityDelegate = startActivityDelegate;
      this.authId = authId;
    }

    StartActivityDelegate getStartActivityDelegate()
    {
      return startActivityDelegate;
    }

    List<String> getPermissions()
    {
      return permissions;
    }

    void setPermissions(List<String> permissions)
    {
      this.permissions = permissions;
    }

    SessionLoginBehavior getLoginBehavior()
    {
      return loginBehavior;
    }

    int getRequestCode()
    {
      return requestCode;
    }

    SessionDefaultAudience getDefaultAudience()
    {
      return defaultAudience;
    }

    String getApplicationId()
    {
      return applicationId;
    }

    boolean isLegacy()
    {
      return isLegacy;
    }

    void setIsLegacy(boolean isLegacy)
    {
      this.isLegacy = isLegacy;
    }

    String getPreviousAccessToken()
    {
      return previousAccessToken;
    }

    boolean needsNewTokenValidation()
    {
      return previousAccessToken != null && !isLegacy;
    }

    String getAuthId()
    {
      return authId;
    }

    boolean isRerequest()
    {
      return isRerequest;
    }

    void setRerequest(boolean isRerequest)
    {
      this.isRerequest = isRerequest;
    }
  }


  static class Result implements Serializable
  {
    private static final long serialVersionUID = 1L;

    enum Code
    {
      SUCCESS("success"),
      CANCEL("cancel"),
      ERROR("error");

      private final String loggingValue;

      Code(String loggingValue)
      {
        this.loggingValue = loggingValue;
      }

      // For consistency across platforms, we want to use specific string values when logging these results.
      String getLoggingValue()
      {
        return loggingValue;
      }
    }

    final Code code;
    final AccessToken token;
    final String errorMessage;
    final String errorCode;
    final AuthorizationRequest request;
    Map<String, String> loggingExtras;

    private Result(AuthorizationRequest request, Code code, AccessToken token, String errorMessage,
                   String errorCode)
    {
      this.request = request;
      this.token = token;
      this.errorMessage = errorMessage;
      this.code = code;
      this.errorCode = errorCode;
    }

    static Result createTokenResult(AuthorizationRequest request, AccessToken token)
    {
      return new Result(request, Code.SUCCESS, token, null, null);
    }

    static Result createCancelResult(AuthorizationRequest request, String message)
    {
      return new Result(request, Code.CANCEL, null, message, null);
    }

    static Result createErrorResult(AuthorizationRequest request, String errorType, String errorDescription)
    {
      return createErrorResult(request, errorType, errorDescription, null);
    }

    static Result createErrorResult(AuthorizationRequest request, String errorType, String errorDescription,
                                    String errorCode)
    {
      String message = TextUtils.join(": ", Utility.asListNoNulls(errorType, errorDescription));
      return new Result(request, Code.ERROR, null, message, errorCode);
    }
  }
}
